﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ProjectHopur23.Repositories;
using ProjectHopur23.Models;

namespace ProjectHopur23.Controllers
{
	public class GameController : Controller
	{
		#region Variables
		private IRepository rep = null;
		#endregion

		#region Constructors
		public GameController()
		{
			rep = new Repository();
		}

		public GameController(IRepository r)
		{
			rep = r;
		}
		#endregion

		/// <summary>
		/// Prepare is a method that takes the ID of a game as a parameter and returns a view where the user prepares for the game s/he chose
		/// The ID is used to return the correct game data to the view
		/// </summary>
		#region Functions
		[HttpGet]
		[Authorize]
		public ActionResult Prepare(int? id)
		{
			if(id == null)
			{
				return View("Error");
			}

			Game g = GetGameByID(id.Value);
			if(g == null)
			{
				return View("Error");
			}

			List<string> players = new List<string>(g.maxPlayers);
			for(int i = 0; i < g.maxPlayers; i++)
			{
				players.Add(String.Empty);
			}

			PrepareGameViewModel p = new PrepareGameViewModel();
			p.theGame = g;
			p.theActiveGame = new ActiveGame();
			p.thePlayers = players;

			return View(p);
		}

		///<summary>
		/// The HttpPost brother of Prepare. This one takes the form and performs server-side error checking.
		/// That includes non-existing usernames, duplicate entries, adding oneself to the game as a player.
		///</summary>
		[HttpPost]
		[Authorize]
		public ActionResult Prepare(PrepareGameViewModel p)
		{
			bool valid = true;
			List<int> playerIDs = new List<int>();
			UserProfile u;

			p.theActiveGame.isFinished = false;
			p.theActiveGame.isFull = false;
			p.theActiveGame.activePlayerId = 0;
			p.theActiveGame.gameId = p.theGame.ID;

			if(String.IsNullOrEmpty(p.theActiveGame.name))
			{
				ModelState.AddModelError("theActiveGame.name", "Þú verður að velja nafn fyrir leikinn");
				valid = false;
			}

			for(int i = 0; i < p.theActiveGame.numberOfPlayers - 1; i++)
			{
				// If the player requested an open game, there is no requirement to fill the list of players
				if(p.theActiveGame.isOpen == true && String.IsNullOrEmpty(p.thePlayers[i]))
				{
					continue;
				}
				else if(p.theActiveGame.isOpen == false && String.IsNullOrEmpty(p.thePlayers[i]))
				{
					ModelState.AddModelError("thePlayers[" + i + "]", "Þú verður að setja leikmann í þennan reit");
					valid = false;
				}
				else
				{
					u = GetUserByName(p.thePlayers[i]);
					if(u == null)
					{
						ModelState.AddModelError("thePlayers[" + i + "]", p.thePlayers[i] + " er ekki skráður á síðunni");
						valid = false;
					}
					else if(p.thePlayers[i] == HttpContext.User.Identity.Name)
					{
						ModelState.AddModelError("thePlayers[" + i + "]", "þú þarft ekki að bæta sjálfum/sjálfri þér við");
						valid = false;
					}
					else
					{
						playerIDs.Add(u.UserId);
					}
				}
			}

			int duplicates = NumDuplicates(playerIDs);
			if(duplicates > 0)
			{
				ModelState.AddModelError("duplicatePlayers", "Ekki má skrá sama leikmann oftar en einu sinni");
				valid = false;
			}

			if(valid == false)
			{
				return View(p);
			}

			rep.AddActiveGame(p.theActiveGame);
			rep.GetAllGames().First(g => g.ID == p.theActiveGame.gameId).timesPlayed += 1;
			rep.Save();
			// Due to the fact the ID is determined by the database, we have to fetch the active game again to know its ID...
			int theInstanceID = GetNewestGameInstance().ID;
			InvitePlayersToGame(theInstanceID, playerIDs);

			// The controller we call is determined by the game's English name, each game controller must therefore implement a Playing method
			return RedirectToAction("Playing", p.theGame.engName, new {id = theInstanceID});
		}

		/// <summary>
		/// GameDescription is a method that takes the ID of a game as a parameter and returns a view where the user prepares for the game s/he chose
		/// The ID is used to return the correct game data to the view
		/// </summary>
		public ActionResult GameDescription(int? id)
		{
			if(id == null)
			{
				return View("Error");
			}

			Game g = GetGameByID(id.Value);

			return View(g);
		}
		#endregion

		//TODO: create a separate class for these misc and database functions? this is getting quite cluttered

		#region MiscFunctions
		///<summary>
		/// GameIsValidInstance will make sure a game is valid, and update invite and player databases to reflect any changes
		/// For example if a user who was invited accepts the invite s/he will enter this function at some point, in which case the invitation needs to be marked as accepted, and the player is added to the players table.
		/// The invitations have a higher priority in open games, so a user can not freely join unless there is a non-invited slot available
		///</summary>
		public bool GameIsValidInstance(ActiveGame ag, string engName, string userName)
		{
			// Does the requested active game even exist?
			if(ag == null)
			{
				return false;
			}

			// Yes it does! Now let's see if this active game is an instance of an existing/correct game in our library
			Game g = GetGameByID(ag.gameId);
			if(g == null || g.engName != engName)
			{
				return false;
			}

			// Ok so that was also successful. Now let's see if the logged-in user is valid
			UserProfile currentUser = GetUserByName(userName);
			if(currentUser == null)
			{
				return false;
			}

			// Yes s/he is. Now let's see if this player is actually a part of this active game
			// And if not, let's see if s/he has any invites to it and has accepted
			Player isPresentInGame = GetPlayerInActiveGameByID(ag.ID, currentUser.UserId);
			if(isPresentInGame == null)
			{
				Invite isInvited = GetInviteForUser(currentUser.UserId, ag.ID);
				// If there are no invites then the user is elegable for free join, assuming the game is not full and is open
				if(isInvited == null)
				{
					if(IsGameFull(ag.ID) == true)
					{
						return false;
					}

					if(ag.isOpen == true)
					{
						AddPlayerToGame(currentUser.UserId, ag);
					}
				}
				// if the user previously rejected an invitation to this game, we don't want to let him/her pass
				else if(isInvited.isAccepted == false && isInvited.isToggled == true)
				{
					return false;
				}
				// The player got an invitation pending so let's assume the clicking of the link is accepting it
				else
				{
					AcceptInvitation(ag.ID, currentUser.UserId);
				}
			}

			// Everything was successful? Let's return a true!
			return true;
		}

		/// <summary>
		/// A simple function that checks for duplicates by running through the same collection in a nested for loop.
		/// Thus to find duplicates it is enough to check whether the return value is above 0 or just 0.
		/// </summary>
		public int NumDuplicates(List<int> UIDs)
		{
			int duplicateCount = 0;
			int count = UIDs.Count;

			for(int i = 0; i < count; i++)
			{
				for(int k = 0; k < count; k++)
				{
					if(i != k && UIDs[k] == UIDs[i])
					{
						duplicateCount++;
					}
				}
			}

			return duplicateCount;
		}
		#endregion

		#region DatabaseFunctions
		/// <summary>
		/// Registers a user as a player in an active game by placing him/her in the Players table.
		/// </summary>
		public void AddPlayerToGame(int uId, ActiveGame ag)
		{
			// No adding to a finished game...
			if(ag.isFinished == true)
			{
				return;
			}

			IEnumerable<UserProfile> players = GetAllUsersInActiveGame(ag.ID);
			int playerCount = players.Count();

			Player p = new Player();
			p.activeGameId = ag.ID;
			p.userId = uId;
			p.playerId = playerCount;

			rep.AddPlayer(p);

			// After adding a player we know for a fact(??) the playerCount is now 1 higher
			// So let's check whether this add reached the numberOfPlayers cap of this game.
			if((playerCount+1) >= ag.numberOfPlayers)
			{
				// TODO: make this redundant database fetch unnecessary by doing it before calling the function
				ActiveGame thisAg = rep.GetAllActiveGames().First(a => a.ID == ag.ID);
				thisAg.isFull = true;
			}

			rep.Save();
		}

		public void RejectInvitation(int agId, int guestId)
		{
			Invite inv = rep.GetAllInvites().First(i => i.activeGameId == agId && i.guestId == guestId);
			inv.isToggled = true;

			ActiveGame ag = rep.GetAllActiveGames().First(a => a.ID == agId);
			// If the game was full, rejecting the invitation opens a slot
			if(ag.isFull == true)
			{
				ag.isFull = false;
			}

			rep.Save();
		}

		public void AcceptInvitation(int agId, int guestId)
		{
			ActiveGame ag = GetActiveGameByID(agId);
			if(ag == null)
			{
				return;
			}

			Invite inv = rep.GetAllInvites().First(i => i.activeGameId == agId && i.guestId == guestId);
			inv.isToggled = true;
			inv.isAccepted = true;
			AddPlayerToGame(guestId, ag);
		}

		// <summary>
		/// IsGameFull counts both amount of players in a game and amount of invites, then sees if they are together
		/// equal or above the numberOfPlayers value specified by the user who created the specified instance.
		/// </summary>
		public bool IsGameFull(int activeGameId)
		{
			ActiveGame ag = rep.GetAllActiveGames().First(a => a.ID == activeGameId);
			int numPlayers = GetNumUsersInActiveGame(activeGameId);
			int numInvites = GetNumInvitesToGame(activeGameId);

			if(numPlayers + numInvites >= ag.numberOfPlayers)
			{
				ag.isFull = true;
				rep.Save();
				return true;
			}
			else
			{
				ag.isFull = false;
				rep.Save();
				return false;
			}
		}

		///<summary>
		/// InvitePlayersToGame will send out invites to the users who own the user IDs in the UIDs list
		/// and they will see an invite on the front page in their invites box.
		///</summary>
		public void InvitePlayersToGame(int activeGameId, List<int> UIDs)
		{
			Invite inv;
			int hostId = GetUserByName(HttpContext.User.Identity.Name).UserId;
			int numPlayers = UIDs.Count();

			//At this point we know all user IDs are valid, so let's invite them
			for(int i = 0; i < numPlayers; i++)
			{
				inv = new Invite();
				inv.activeGameId = activeGameId;
				inv.guestId = UIDs[i];
				inv.hostId = hostId;
				inv.isToggled = false;
				inv.isAccepted = false;
				rep.AddInvite(inv);
			}

			// Add the player (aka the host) who created the game to the player list, s/he is always player 0 in the game
			Player p = new Player();
			p.activeGameId = activeGameId;
			p.userId = hostId;
			p.playerId = 0;
			rep.AddPlayer(p);

			ActiveGame ag = rep.GetAllActiveGames().First(a => a.ID == activeGameId);
			if(numPlayers + 1 == ag.numberOfPlayers)
			{
				ag.isFull = true;
			}

			rep.Save();
		}

		public Player GetPlayerInActiveGameByID(int agId, int uId)
		{
			Player thePlayer = (from p in rep.GetAllPlayers()
					where p.activeGameId == agId && p.userId == uId
					select p).FirstOrDefault();

			return thePlayer;
		}

		public int GetNumUsersInActiveGame(int id)
		{
			int theCount = (from u in GetAllUsersInActiveGame(id)
					select u).Count();

			return theCount;
		}

		public IEnumerable<UserProfile> GetHighScores(int amount)
		{
			IEnumerable<UserProfile> theUsers = (from u in rep.GetAllUsers()
							orderby u.score descending
							select u).Take(amount);

			return theUsers;
		}

		public IEnumerable<UserProfile> GetAllUsersInActiveGame(int activeGameID)
		{
			IEnumerable<UserProfile> theUsers = from u in rep.GetAllUsers()
							join p in rep.GetAllPlayers() on u.UserId equals p.userId
							where p.activeGameId == activeGameID
							orderby p.playerId
							select u;

			return theUsers;
		}

		public Invite GetInviteForUser(int uId, int agId)
		{
			Invite theInvite = (from i in rep.GetAllInvites()
						where i.guestId == uId && i.activeGameId == agId
						select i).FirstOrDefault();

			return theInvite;
		}

		public int GetNumInvitesToGame(int agId)
		{
			int numInvites = (from i in rep.GetAllInvites()
					where i.activeGameId == agId && i.isToggled == false
					select i).Count();

			return numInvites;
		}

		public ActiveGame GetActiveGameByID(int id)
		{
			ActiveGame theActiveGame = (from ag in rep.GetAllActiveGames()
							where ag.ID == id
							select ag).FirstOrDefault();

			return theActiveGame;
		}

		public ActiveGame GetNewestGameInstance()
		{
			ActiveGame theInstance = (from ag in rep.GetAllActiveGames()
							orderby ag.ID descending
							select ag).FirstOrDefault();

			return theInstance;
		}

		public Game GetGameByID(int id)
		{
			Game theGame = (from g in rep.GetAllGames()
							where g.ID == id
							select g).FirstOrDefault();

			return theGame;
		}

		public Game GetGameByName(string gname)
		{
			Game theGame = (from g in rep.GetAllGames()
						where g.name == gname
						select g).FirstOrDefault();

			return theGame;
		}

		public UserProfile GetUserByID(int userID)
		{
			UserProfile theUser = (from u in rep.GetAllUsers()
						where u.UserId == userID
						select u).FirstOrDefault();

			return theUser;
		}

		public UserProfile GetUserByName(string userName)
		{
			UserProfile theUser = (from u in rep.GetAllUsers()
						where u.UserName == userName
						select u).FirstOrDefault();

			return theUser;
		}

		public IEnumerable<UserProfile> GetOnlineUsers()
		{
			IEnumerable<UserProfile> theUsers = from u in rep.GetAllUsers()
								where u.isLoggedIn == true
								select u;

			return theUsers;
		}
		#endregion
	}
}
